1use anyhow::{Context, Error};
6use component_events::events::*;
7use component_events::matcher::*;
8use component_events::sequence::*;
9use diagnostics_data::{Data, Logs};
10use diagnostics_reader::{ArchiveReader, LogsArchiveReader};
11use fuchsia_async as fasync;
12use fuchsia_component_test::{Capability, ChildOptions, ChildRef, RealmBuilder, Ref, Route};
13use regex::Regex;
14use std::future::Future;
15
16pub trait Component {
19 fn get_name(&self) -> String;
20 fn get_path(&self) -> String;
21 fn get_regex_matcher(&self) -> String;
22 fn matches_log(&self, raw_log: &Data<Logs>) -> bool;
23}
24
25#[derive(Clone)]
27pub struct Client<'a> {
28 name: String,
29 path: &'a str,
30 regex: Regex,
31 matcher: String,
32}
33
34impl<'a> Client<'a> {
35 pub fn new(test_prefix: &str, path: &'a str) -> Self {
42 let name = format!("{}_client", test_prefix);
43 let matcher = format!("{}$", name);
44 Self { regex: Regex::new(matcher.as_str()).unwrap(), name, path, matcher }
45 }
46}
47
48impl<'a> Component for Client<'a> {
49 fn get_name(&self) -> String {
50 self.name.clone()
51 }
52 fn get_path(&self) -> String {
53 self.path.to_owned()
54 }
55 fn get_regex_matcher(&self) -> String {
56 self.matcher.clone()
57 }
58 fn matches_log(&self, raw_log: &Data<Logs>) -> bool {
59 self.regex.is_match(raw_log.moniker.to_string().as_ref())
60 }
61}
62
63#[derive(Clone)]
65pub struct Proxy<'a> {
66 name: String,
67 path: &'a str,
68 regex: Regex,
69 matcher: String,
70}
71
72impl<'a> Proxy<'a> {
73 pub fn new(test_prefix: &str, path: &'a str) -> Self {
80 let name = format!("{}_proxy", test_prefix);
81 let matcher = format!("{}$", name);
82 Self { regex: Regex::new(matcher.as_str()).unwrap(), name, path, matcher }
83 }
84}
85
86impl<'a> Component for Proxy<'a> {
87 fn get_name(&self) -> String {
88 self.name.clone()
89 }
90 fn get_path(&self) -> String {
91 self.path.to_string()
92 }
93 fn get_regex_matcher(&self) -> String {
94 self.matcher.clone()
95 }
96 fn matches_log(&self, raw_log: &Data<Logs>) -> bool {
97 self.regex.is_match(raw_log.moniker.to_string().as_ref())
98 }
99}
100
101#[derive(Clone)]
103pub struct Server<'a> {
104 name: String,
105 path: &'a str,
106 regex: Regex,
107 matcher: String,
108}
109
110impl<'a> Server<'a> {
111 pub fn new(test_prefix: &str, path: &'a str) -> Self {
118 let name = format!("{}_server", test_prefix);
119 let matcher = format!("{}$", name);
120 Self { regex: Regex::new(matcher.as_str()).unwrap(), name, path, matcher }
121 }
122}
123
124impl<'a> Component for Server<'a> {
125 fn get_name(&self) -> String {
126 self.name.clone()
127 }
128 fn get_path(&self) -> String {
129 self.path.to_string()
130 }
131 fn get_regex_matcher(&self) -> String {
132 self.matcher.clone()
133 }
134 fn matches_log(&self, raw_log: &Data<Logs>) -> bool {
135 self.regex.is_match(raw_log.moniker.to_string().as_ref())
136 }
137}
138
139pub enum TestKind<'a> {
144 StandaloneComponent { client: &'a Client<'a> },
145 ClientAndServer { client: &'a Client<'a>, server: &'a Server<'a> },
146 ClientProxyAndServer { client: &'a Client<'a>, proxy: &'a Proxy<'a>, server: &'a Server<'a> },
147}
148
149pub async fn run_test<'a, Fut, FutLogsReader>(
157 protocol_name: &str,
158 test_kind: TestKind<'a>,
159 input_setter: impl FnOnce(RealmBuilder, ChildRef) -> Fut,
160 logs_reader: impl Fn(LogsArchiveReader) -> FutLogsReader,
161) -> Result<(), Error>
162where
163 Fut: Future<Output = Result<(RealmBuilder, ChildRef), Error>> + 'a,
164 FutLogsReader: Future<Output = ()> + 'a,
165{
166 let event_stream = EventStream::open().await.unwrap();
168
169 let builder = RealmBuilder::new().await?;
171
172 let (client_name, client_path, client_regex_matcher) = match test_kind {
174 TestKind::StandaloneComponent { client, .. }
175 | TestKind::ClientAndServer { client, .. }
176 | TestKind::ClientProxyAndServer { client, .. } => {
177 (client.get_name(), client.get_path(), client.get_regex_matcher())
178 }
179 };
180 let client =
181 builder.add_child(client_name.clone(), client_path, ChildOptions::new().eager()).await?;
182
183 let (builder, client) = input_setter(builder, client).await?;
185
186 let mut log_sink_route = Route::new()
188 .capability(Capability::protocol_by_name("fuchsia.logger.LogSink"))
189 .from(Ref::parent())
190 .to(&client);
191
192 let mut child_names = vec![client_name];
194
195 let mut start_event_matchers =
197 vec![EventMatcher::ok().moniker_regex(client_regex_matcher.clone())];
198
199 if !std::matches!(test_kind, TestKind::StandaloneComponent { .. }) {
201 let (server_name, server_path, server_regex_matcher) = match test_kind {
203 TestKind::ClientAndServer { server, .. }
204 | TestKind::ClientProxyAndServer { server, .. } => {
205 (server.get_name(), server.get_path(), server.get_regex_matcher())
206 }
207 _ => panic!("unreachable!"),
208 };
209 let server =
210 builder.add_child(server_name.clone(), server_path, ChildOptions::new()).await?;
211 child_names.push(server_name);
212
213 log_sink_route = log_sink_route.to(&server);
215
216 start_event_matchers.push(EventMatcher::ok().moniker_regex(server_regex_matcher));
218
219 if std::matches!(test_kind, TestKind::ClientAndServer { .. }) {
220 builder
222 .add_route(
223 Route::new()
224 .capability(Capability::protocol_by_name(protocol_name))
225 .from(&server)
226 .to(&client),
227 )
228 .await?;
229 } else {
230 let (proxy_name, proxy_path, proxy_regex_matcher) = match test_kind {
232 TestKind::ClientProxyAndServer { proxy, .. } => {
233 (proxy.get_name(), proxy.get_path(), proxy.get_regex_matcher())
234 }
235 _ => panic!("unreachable!"),
236 };
237 let proxy =
238 builder.add_child(proxy_name.clone(), proxy_path, ChildOptions::new()).await?;
239 child_names.insert(1, proxy_name);
240
241 log_sink_route = log_sink_route.to(&proxy);
243
244 start_event_matchers.insert(1, EventMatcher::ok().moniker_regex(proxy_regex_matcher));
248
249 builder
251 .add_route(
252 Route::new()
253 .capability(Capability::protocol_by_name(protocol_name))
254 .from(&server)
255 .to(&proxy),
256 )
257 .await?;
258
259 builder
261 .add_route(
262 Route::new()
263 .capability(Capability::protocol_by_name(protocol_name))
264 .from(&proxy)
265 .to(&client),
266 )
267 .await?;
268 }
269 }
270
271 builder.add_route(log_sink_route.to_owned()).await?;
273
274 let realm_instance = builder.build().await?;
276
277 EventSequence::new()
279 .has_subset(start_event_matchers, Ordering::Unordered)
280 .has_subset(
281 vec![EventMatcher::ok()
282 .stop(Some(ExitStatusMatcher::Clean))
283 .moniker_regex(client_regex_matcher)],
284 Ordering::Unordered,
285 )
286 .expect(event_stream)
287 .await
288 .unwrap();
289
290 let mut archivist_reader = ArchiveReader::logs();
292 child_names.iter().for_each(|child_name| {
293 let moniker = format!("realm_builder:{}/{}", realm_instance.root.child_name(), child_name);
294 archivist_reader.select_all_for_component(moniker.as_str());
295 });
296
297 realm_instance.destroy().await?;
299
300 logs_reader(archivist_reader);
302 Ok(())
303}
304
305pub fn logs_to_str<'a>(
312 raw_logs: &'a Vec<Data<Logs>>,
313 maybe_filter_by_process: Option<Vec<&'a dyn Component>>,
314) -> impl Iterator<Item = &'a str> + 'a {
315 logs_to_str_filtered(raw_logs, maybe_filter_by_process, |_raw_log| true)
316}
317
318pub fn logs_to_str_filtered<'a>(
322 raw_logs: &'a Vec<Data<Logs>>,
323 maybe_filter_by_process: Option<Vec<&'a dyn Component>>,
324 filter_by_log: impl FnMut(&&Data<Logs>) -> bool + 'a,
325) -> impl Iterator<Item = &'a str> + 'a {
326 raw_logs
327 .iter()
328 .filter(move |raw_log| match maybe_filter_by_process {
329 Some(ref process_list) => {
330 for process in process_list.iter() {
331 if process.matches_log(*raw_log) {
332 return true;
333 }
334 }
335 return false;
336 }
337 None => true,
338 })
339 .filter(filter_by_log)
340 .map(|raw_log| {
341 raw_log.payload_message().expect("payload not found").properties[0]
342 .string()
343 .expect("message is not string")
344 })
345}
346
347pub async fn assert_logs_eq_to_golden<'a>(
357 log_reader: &'a LogsArchiveReader,
358 comp: &'a dyn Component,
359) {
360 assert_filtered_logs_eq_to_golden(&log_reader, comp, |_raw_log| true).await;
361}
362
363pub async fn assert_filtered_logs_eq_to_golden<'a>(
367 log_reader: &'a LogsArchiveReader,
368 comp: &'a dyn Component,
369 filter_by_log: impl FnMut(&&Data<Logs>) -> bool + 'a + Copy,
370) {
371 let golden_path = format!("/pkg/data/goldens/{}.log.golden", comp.get_name());
373 let golden_file = std::fs::read_to_string(golden_path.clone())
374 .with_context(|| format!("Failed to load {golden_path}"))
375 .unwrap();
376 let golden_logs = golden_file.as_str().trim();
377
378 const MAX_ATTEMPTS: usize = 10;
379 let mut attempts = 0;
380 while attempts < MAX_ATTEMPTS {
381 attempts += 1;
382 let raw_logs = log_reader.snapshot().await.expect("can read from the accessor");
383
384 let logs = logs_to_str_filtered(&raw_logs, Some(vec![comp]), filter_by_log)
386 .collect::<Vec<&str>>()
387 .join("\n");
388 if logs == golden_logs.trim() {
389 break;
390 } else if attempts == MAX_ATTEMPTS {
391 print!(
392 "
393
394Logs golden mismatch in '{}' ({})
395Please copy the output between the '===' bounds into the golden file at {} in the fuchsia.git tree
396====================================================================================================
397{}
398====================================================================================================
399
400
401",
402 comp.get_name(),
403 comp.get_path(),
404 golden_path,
405 logs
406 );
407 assert_eq!(logs, golden_logs)
408 }
409 fasync::Timer::new(fasync::MonotonicInstant::after(zx::MonotonicDuration::from_millis(
410 500,
411 )))
412 .await;
413 }
414}