Skip to main content

test_runners_test_lib/
test_lib.rs

1// Copyright 2020 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 anyhow::{Context, Error, format_err};
6use fidl::endpoints;
7use fidl::endpoints::{ClientEnd, Proxy};
8use fidl_fuchsia_component as fcomponent;
9use fidl_fuchsia_component_decl as fdecl;
10use fidl_fuchsia_component_runner as fcrunner;
11use fidl_fuchsia_io as fio;
12use fidl_fuchsia_test::CaseListenerRequest::Finished;
13use fidl_fuchsia_test::RunListenerRequest::{OnFinished, OnTestCaseStarted};
14use fidl_fuchsia_test::{Invocation, Result_ as TestResult, RunListenerRequestStream};
15use fidl_fuchsia_test_manager as ftest_manager;
16use fuchsia_async as fasync;
17use fuchsia_component::client::{self, connect_to_protocol_at_dir_root};
18use fuchsia_runtime::job_default;
19use futures::channel::mpsc;
20use futures::prelude::*;
21use namespace::{Namespace, NamespaceError};
22use std::collections::HashMap;
23use std::sync::Arc;
24use test_manager_test_lib::RunEvent;
25use test_runners_lib::elf::{BuilderArgs, Component};
26
27#[derive(PartialEq, Debug)]
28pub enum ListenerEvent {
29    StartTest(String),
30    FinishTest(String, TestResult),
31    FinishAllTests,
32}
33
34fn get_ord_index_and_name(event: &ListenerEvent) -> (usize, &str) {
35    match event {
36        ListenerEvent::StartTest(name) => (0, name),
37        ListenerEvent::FinishTest(name, _) => (1, name),
38        ListenerEvent::FinishAllTests => (2, ""),
39    }
40}
41
42// Orders by test name and then event type.
43impl Ord for ListenerEvent {
44    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
45        let (s_index, s_test_name) = get_ord_index_and_name(self);
46        let (o_index, o_test_name) = get_ord_index_and_name(other);
47        if s_test_name == o_test_name || s_index == 2 || o_index == 2 {
48            return s_index.cmp(&o_index);
49        }
50        return s_test_name.cmp(&o_test_name);
51    }
52}
53
54// Makes sure that FinishTest event never shows up before StartTest and FinishAllTests is always
55// last.
56pub fn assert_event_ord(events: &Vec<ListenerEvent>) {
57    let mut tests = HashMap::new();
58    let mut all_finish = false;
59    for event in events {
60        assert!(!all_finish, "got FinishAllTests event twice: {:#?}", events);
61        match event {
62            ListenerEvent::StartTest(name) => {
63                assert!(
64                    !tests.contains_key(&name),
65                    "Multiple StartTest for test {}: {:#?}",
66                    name,
67                    events
68                );
69                tests.insert(name, false);
70            }
71            ListenerEvent::FinishTest(name, _) => {
72                assert!(
73                    tests.contains_key(&name),
74                    "Got finish before start event for test {}: {:#?}",
75                    name,
76                    events
77                );
78                assert!(
79                    !tests.insert(name, true).unwrap(),
80                    "Multiple FinishTest for test {}: {:#?}",
81                    name,
82                    events
83                );
84            }
85            ListenerEvent::FinishAllTests => {
86                all_finish = true;
87            }
88        }
89    }
90}
91
92impl PartialOrd for ListenerEvent {
93    fn partial_cmp(&self, other: &ListenerEvent) -> Option<core::cmp::Ordering> {
94        Some(self.cmp(other))
95    }
96}
97
98impl Eq for ListenerEvent {}
99
100impl ListenerEvent {
101    pub fn start_test(name: &str) -> ListenerEvent {
102        ListenerEvent::StartTest(name.to_string())
103    }
104    pub fn finish_test(name: &str, test_result: TestResult) -> ListenerEvent {
105        ListenerEvent::FinishTest(name.to_string(), test_result)
106    }
107    pub fn finish_all_test() -> ListenerEvent {
108        ListenerEvent::FinishAllTests
109    }
110}
111
112impl Clone for ListenerEvent {
113    fn clone(&self) -> Self {
114        match self {
115            ListenerEvent::StartTest(name) => ListenerEvent::start_test(name),
116            ListenerEvent::FinishTest(name, test_result) => ListenerEvent::finish_test(
117                name,
118                TestResult { status: test_result.status.clone(), ..Default::default() },
119            ),
120            ListenerEvent::FinishAllTests => ListenerEvent::finish_all_test(),
121        }
122    }
123}
124
125/// Collects all the listener event as they come and return in a vector.
126pub async fn collect_listener_event(
127    mut listener: RunListenerRequestStream,
128) -> Result<Vec<ListenerEvent>, Error> {
129    let mut ret = vec![];
130    // collect loggers so that they do not die.
131    let mut loggers = vec![];
132    while let Some(result_event) = listener.try_next().await? {
133        match result_event {
134            OnTestCaseStarted { invocation, std_handles, listener, .. } => {
135                let name = invocation.name.unwrap();
136                ret.push(ListenerEvent::StartTest(name.clone()));
137                loggers.push(std_handles);
138                let mut listener = listener.into_stream();
139                // We want exhaustive match, and if we add more variants in the future we'd need to
140                // handle the requests in a loop, so allow this lint violation.
141                #[allow(clippy::never_loop)]
142                while let Some(result) = listener.try_next().await? {
143                    match result {
144                        Finished { result, .. } => {
145                            ret.push(ListenerEvent::FinishTest(name, result));
146                            break;
147                        }
148                    }
149                }
150            }
151            OnFinished { .. } => {
152                ret.push(ListenerEvent::FinishAllTests);
153                break;
154            }
155        }
156    }
157    Ok(ret)
158}
159
160/// Helper method to convert names to `Invocation`.
161pub fn names_to_invocation(names: Vec<&str>) -> Vec<Invocation> {
162    names
163        .iter()
164        .map(|s| Invocation { name: Some(s.to_string()), tag: None, ..Default::default() })
165        .collect()
166}
167
168// process events by parsing and normalizing logs. Returns `RunEvents` and collected logs.
169pub async fn process_events(
170    suite_instance: test_manager_test_lib::SuiteRunInstance,
171    exclude_empty_logs: bool,
172) -> Result<(Vec<RunEvent>, Vec<String>), Error> {
173    let (sender, mut recv) = mpsc::channel(1);
174    let execution_task = fasync::Task::spawn(async move {
175        suite_instance.collect_events_with_watch(sender, true, true).await
176    });
177    let mut events = vec![];
178    let mut log_tasks = vec![];
179    let mut buffered_stdout = HashMap::new();
180    let mut buffered_stderr = HashMap::new();
181    while let Some(event) = recv.next().await {
182        match event.payload {
183            test_manager_test_lib::SuiteEventPayload::RunEvent(RunEvent::CaseStdout {
184                name,
185                stdout_message,
186            }) => {
187                let strings = line_buffer_std_message(
188                    &name,
189                    stdout_message,
190                    exclude_empty_logs,
191                    &mut buffered_stdout,
192                );
193                for s in strings {
194                    events.push(RunEvent::case_stdout(name.clone(), s));
195                }
196            }
197            test_manager_test_lib::SuiteEventPayload::RunEvent(RunEvent::CaseStderr {
198                name,
199                stderr_message,
200            }) => {
201                let strings = line_buffer_std_message(
202                    &name,
203                    stderr_message,
204                    exclude_empty_logs,
205                    &mut buffered_stderr,
206                );
207                for s in strings {
208                    events.push(RunEvent::case_stderr(name.clone(), s));
209                }
210            }
211            test_manager_test_lib::SuiteEventPayload::RunEvent(e) => events.push(e),
212            test_manager_test_lib::SuiteEventPayload::SuiteLog { log_stream } => {
213                let t = fasync::Task::spawn(log_stream.collect::<Vec<_>>());
214                log_tasks.push(t);
215            }
216            test_manager_test_lib::SuiteEventPayload::TestCaseLog { .. } => {
217                panic!("not supported yet!")
218            }
219            test_manager_test_lib::SuiteEventPayload::DebugData { .. } => {
220                panic!("not supported yet!")
221            }
222        }
223    }
224    execution_task.await.context("test execution failed")?;
225
226    for (name, log) in buffered_stdout {
227        events.push(RunEvent::case_stdout(name, log));
228    }
229    for (name, log) in buffered_stderr {
230        events.push(RunEvent::case_stderr(name, log));
231    }
232
233    let mut collected_logs = vec![];
234    for t in log_tasks {
235        let logs = t.await;
236        for log_result in logs {
237            let log = log_result?;
238            collected_logs.push(log.msg().unwrap().to_string());
239        }
240    }
241
242    Ok((events, collected_logs))
243}
244
245// Process stdout/stderr messages and return Vec of processed strings
246fn line_buffer_std_message(
247    name: &str,
248    std_message: String,
249    exclude_empty_logs: bool,
250    buffer: &mut HashMap<String, String>,
251) -> Vec<String> {
252    let mut ret = vec![];
253    let logs = std_message.split("\n");
254    let mut logs = logs.collect::<Vec<&str>>();
255    // discard last empty log(if it ended in newline, or  store im-complete line)
256    let mut last_incomplete_line = logs.pop();
257    if std_message.as_bytes().last() == Some(&b'\n') {
258        last_incomplete_line = None;
259    }
260    for log in logs {
261        if exclude_empty_logs && log.len() == 0 {
262            continue;
263        }
264        let mut msg = log.to_owned();
265        // This is only executed for first log line and used to concat previous
266        // buffered line.
267        if let Some(prev_log) = buffer.remove(name) {
268            msg = format!("{}{}", prev_log, msg);
269        }
270        ret.push(msg);
271    }
272    if let Some(log) = last_incomplete_line {
273        let mut log = log.to_owned();
274        if let Some(prev_log) = buffer.remove(name) {
275            log = format!("{}{}", prev_log, log);
276        }
277        buffer.insert(name.to_string(), log);
278    }
279    ret
280}
281
282// Binds to test manager component and returns suite runner service.
283pub async fn connect_to_suite_runner() -> Result<ftest_manager::SuiteRunnerProxy, Error> {
284    let realm = client::connect_to_protocol::<fcomponent::RealmMarker>()
285        .context("could not connect to Realm service")?;
286
287    let child_ref = fdecl::ChildRef { name: "test_manager".to_owned(), collection: None };
288    let (dir, server_end) = endpoints::create_proxy::<fio::DirectoryMarker>();
289    realm
290        .open_exposed_dir(&child_ref, server_end)
291        .await
292        .context("open_exposed_dir fidl call failed for test manager")?
293        .map_err(|e| format_err!("failed to create test manager: {:?}", e))?;
294
295    connect_to_protocol_at_dir_root::<ftest_manager::SuiteRunnerMarker>(&dir)
296        .context("failed to open test suite runner service")
297}
298
299fn create_ns_from_current_ns(
300    dir_paths: Vec<(&str, fio::Flags)>,
301) -> Result<Namespace, NamespaceError> {
302    let mut ns = vec![];
303    for (path, permission) in dir_paths {
304        let chan = fuchsia_fs::directory::open_in_namespace(path, permission)
305            .unwrap()
306            .into_channel()
307            .unwrap()
308            .into_zx_channel();
309        let handle = ClientEnd::new(chan);
310
311        ns.push(fcrunner::ComponentNamespaceEntry {
312            path: Some(path.to_string()),
313            directory: Some(handle),
314            ..Default::default()
315        });
316    }
317    Namespace::try_from(ns)
318}
319
320/// Create a new component object for testing purposes.
321pub async fn test_component(
322    url: &str,
323    name: &str,
324    binary: &str,
325    args: Vec<String>,
326) -> Result<Arc<Component>, Error> {
327    let ns = create_ns_from_current_ns(vec![
328        ("/pkg", fuchsia_fs::PERM_READABLE | fuchsia_fs::PERM_EXECUTABLE),
329        // TODO(b/376735013): Restrict this to LogSink instead of all of /svc.
330        ("/svc", fuchsia_fs::PERM_READABLE | fuchsia_fs::PERM_EXECUTABLE),
331    ])?;
332    let component = Component::create_for_tests(BuilderArgs {
333        url: url.to_string(),
334        name: name.to_string(),
335        binary: binary.to_string(),
336        args,
337        environ: None,
338        ns,
339        job: job_default().duplicate_handle(zx::Rights::SAME_RIGHTS)?,
340        options: zx::ProcessOptions::empty(),
341        config: None,
342    })
343    .await?;
344    Ok(Arc::new(component))
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350    use fidl_fuchsia_test::Status;
351    use maplit::hashmap;
352
353    #[test]
354    fn test_ordering_by_enum() {
355        let expected_events = vec![
356            ListenerEvent::start_test("a"),
357            ListenerEvent::finish_test(
358                "a",
359                TestResult { status: Some(Status::Passed), ..Default::default() },
360            ),
361            ListenerEvent::finish_all_test(),
362        ];
363
364        let mut events = expected_events.clone();
365        events.reverse();
366
367        assert_ne!(events, expected_events);
368        events.sort();
369        assert_eq!(events, expected_events);
370    }
371
372    #[test]
373    fn test_ordering_by_test_name() {
374        let mut events = vec![
375            ListenerEvent::start_test("b"),
376            ListenerEvent::start_test("a"),
377            ListenerEvent::finish_test(
378                "a",
379                TestResult { status: Some(Status::Passed), ..Default::default() },
380            ),
381            ListenerEvent::start_test("c"),
382            ListenerEvent::finish_test(
383                "b",
384                TestResult { status: Some(Status::Passed), ..Default::default() },
385            ),
386            ListenerEvent::finish_test(
387                "c",
388                TestResult { status: Some(Status::Passed), ..Default::default() },
389            ),
390            ListenerEvent::finish_all_test(),
391        ];
392
393        let expected_events = vec![
394            ListenerEvent::start_test("a"),
395            ListenerEvent::finish_test(
396                "a",
397                TestResult { status: Some(Status::Passed), ..Default::default() },
398            ),
399            ListenerEvent::start_test("b"),
400            ListenerEvent::finish_test(
401                "b",
402                TestResult { status: Some(Status::Passed), ..Default::default() },
403            ),
404            ListenerEvent::start_test("c"),
405            ListenerEvent::finish_test(
406                "c",
407                TestResult { status: Some(Status::Passed), ..Default::default() },
408            ),
409            ListenerEvent::finish_all_test(),
410        ];
411        events.sort();
412        assert_eq!(events, expected_events);
413    }
414
415    #[test]
416    fn line_buffer_std_message_incomplete_line() {
417        let mut buf = HashMap::new();
418        buf.insert("test".to_string(), "some_prev_text".to_string());
419        let strings = line_buffer_std_message("test", "a \nb\nc\nd".into(), false, &mut buf);
420        assert_eq!(strings, vec!["some_prev_texta ".to_owned(), "b".to_owned(), "c".to_owned()]);
421        assert_eq!(buf, hashmap! {"test".to_string() => "d".to_string()});
422    }
423
424    #[test]
425    fn line_buffer_std_message_complete_line() {
426        let mut buf = HashMap::new();
427        buf.insert("test".to_string(), "some_prev_text".to_string());
428        let strings = line_buffer_std_message("test", "a \nb\nc\n".into(), false, &mut buf);
429        assert_eq!(strings, vec!["some_prev_texta ".to_owned(), "b".to_owned(), "c".to_owned()]);
430        assert_eq!(buf.len(), 0);
431
432        // test when initial buf is empty
433        let strings = line_buffer_std_message("test", "d \ne\nf\n".into(), false, &mut buf);
434        assert_eq!(strings, vec!["d ".to_owned(), "e".to_owned(), "f".to_owned()]);
435        assert_eq!(buf.len(), 0);
436    }
437}